package m3f.retrieval;

import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.FileReader;
import java.io.FileWriter;
import java.io.IOException;
import java.util.ArrayList;

import m3f.data.SingleDataProvider;
import m3f.data.SingleDataSet;

import org.jblas.DoubleMatrix;

public class Rank{
	
	private static SingleDataProvider database;
	private static DoubleMatrix queries;
	private ArrayList<String> databaseNames;
	private ArrayList<String> queryNames;
	private static boolean combined = false;
	private SimilarityMeasure measure;
	private static ArrayList<String[]> combinedQueries;
	private static ArrayList<int[]> combinedQueriesIndexes;
	private static ArrayList<String> combinedQueriesNames;
	private static Operator operator;
	private static DoubleMatrix R;
	
	public static enum Operator{avg, max, qsum, rsum};
	public static enum SimilarityMeasure{dotProduct, histogramIntersection, tanimoto};
	private static boolean debug = false;
	
	
	public Rank(SingleDataProvider database, SingleDataSet queries, String databaseNamesFile, String queryNamesFile, SimilarityMeasure measure){
		Rank.database = database;
		Rank.queries = queries.fullBatch().matrix();
		this.databaseNames = loadNames(databaseNamesFile);
		this.queryNames = loadNames(queryNamesFile);
		this.measure = measure;
		Rank.combined = false;
	}
	
	public void ranking(String combinedQueriesfile, Operator operator){
		
		combined = true;
		Rank.operator = operator;
		combinedQueries = loadCombinedNames(combinedQueriesfile);
		combinedQueriesNames = new ArrayList<String>();
		
		combinedQueriesIndexes = new ArrayList<int[]>();
		for (int i = 0; i < combinedQueries.size(); i++) {
			int[] indexes = new int[combinedQueries.get(i).length - 1];
			for (int j = 0; j < combinedQueries.get(i).length - 1; j++) {
				for (int k = 0; k < queryNames.size(); k++) {
					if (combinedQueries.get(i)[j + 1].equals(queryNames.get(k))) {
						indexes[j] = k;
					}
				}
			}
			combinedQueriesNames.add(combinedQueries.get(i)[0]);
			combinedQueriesIndexes.add(indexes);
		}
		printMatrix("Q", queries);
		
		if (operator == Operator.qsum) {
			System.out.println("is combined with ");
			System.out.println("rows" + queries.rows + " cols" + queries.columns);
			DoubleMatrix combinedQueries = new DoubleMatrix(queries.rows, combinedQueriesIndexes.size());
			for (int i = 0; i < combinedQueriesIndexes.size(); i ++) {
				for (int j = 0; j < queries.rows; j++){
					double sum = 0;
					for (int dim : combinedQueriesIndexes.get(i)) {
						sum += queries.get(j, dim);
					}
					combinedQueries.put(j, i, sum);
				}
			}
			queries = combinedQueries;
			printMatrix("combinedQueries", combinedQueries);
		}
		
		print("combinedQueriesIndexes.size", combinedQueriesIndexes.size());
		
		ranking();
		
	}
	
	public static void initMatrix(){
		if (combined){
			R = DoubleMatrix.zeros(database.matrixRows(), combinedQueriesIndexes.size());
		} else{
			R = DoubleMatrix.zeros(database.matrixRows(), queries.columns);
		}
	}
	
	public void ranking() {
		
		database.reset(false);
		int minibatch = 1000;
		database.setMinibatchSize(minibatch);
				
		ConcurrentRank cr = new ConcurrentRank(database, queries, measure);
		
		Thread thread = new Thread(cr);
		thread.start();
		try{
			thread.join();
		}catch(InterruptedException ie){
			ie.printStackTrace();
		}
		 
	}
	
	public static DoubleMatrix dotProductRanking(DoubleMatrix db, DoubleMatrix queries){
	    return db.transpose().mmul(queries);
	}
	
	public static DoubleMatrix histogramIntersectionRanking(DoubleMatrix db, DoubleMatrix queries){
		DoubleMatrix P = DoubleMatrix.zeros(db.columns, queries.columns);
		for (int i = 0; i < db.columns; i++){
			for (int j = 0; j < queries.columns; j++){
				P.put(i, j, db.getColumn(i).mini(queries.getColumn(j)).sum());
			}
		}
		return P;
	}
	
	public static DoubleMatrix tanimotoRanking(DoubleMatrix db, DoubleMatrix queries){
		
		
		DoubleMatrix P = DoubleMatrix.zeros(db.columns, queries.columns);
		DoubleMatrix AA;
		DoubleMatrix AB;
		DoubleMatrix BB;
		for (int i = 0; i < db.columns; i++){
			AA = db.getColumn(i).transpose().mmul(db.getColumn(i));
			for (int j = 0; j < queries.columns; j++){
				AB = db.getColumn(i).transpose().mmul(queries.getColumn(j));
				BB = queries.getColumn(j).transpose().mmul(queries.getColumn(j));
				P.put(i, j, AB.get(0) / (AA.get(0) + BB.get(0) - AB.get(0)));
			}
		}
		return P;
	}
	
	public static void print(String name, Object object) {
		if (debug) {
			System.out.println(name + ": " + object);
		}
	}
	
	public static void printMatrix(String name, DoubleMatrix M) {
		if (debug) {
		System.out.println("\n" + name + ":");
			for (int i = 0; i < M.columns; i++) {
				for (int j = 0; j < M.rows; j++) {
					System.out.print(M.get(j, i) + " ");
				}
				System.out.println();
			}
		}
	}
	public static void rank(DoubleMatrix db, DoubleMatrix queries, SimilarityMeasure measure, int batchSize){
			
		DoubleMatrix C;
		printMatrix("DB", db);
		printMatrix("Q", queries);

	    switch (measure) {
		case dotProduct:
			C =  dotProductRanking(db, queries);
			break;
		case histogramIntersection:
			C =  histogramIntersectionRanking(db, queries);
			break;
		case tanimoto:
			C = tanimotoRanking(db, queries);
			break;
		default:
			C =  dotProductRanking(db, queries);
			break;
		}
	    
	    printMatrix("C", C);
	    
	    if (combined && operator != Operator.qsum) {
	    	DoubleMatrix CC = DoubleMatrix.zeros(C.rows, combinedQueries.size());
			for (int i = 0; i < combinedQueriesIndexes.size(); i++) {
				for (int j = 0; j < C.rows; j++) {
					CC.put(j, i, combineScores(j, C, combinedQueriesIndexes.get(i), operator));
				}
			}
			C = CC;
	    }
	    
		int pointer = C.rows * batchSize;
		int dbRows;
		for(dbRows = 0; dbRows < C.rows; dbRows++) {
			for(int j = 0; j < C.columns; j++) {
				double d = C.get(dbRows, j);
				R.put(dbRows + pointer, j, d);
			}
		}
		printMatrix("R", R);
	    
	}
	
	/**
	 * 
	 * @param R ranking matrix
	 * @param outputFile output file address
	 */
	public void writeTrecEvalTopFile(String outputFile, int maxResults){
		
		int results[][] = R.columnSortingPermutations();
		
		System.out.println("Results(" + results.length+","+results[0].length+")");
		System.out.println("QueryNames(" + queryNames.size()+")");
		System.out.println("DataBaseNames(" + databaseNames.size()+")");
		try {
			BufferedWriter writer = new BufferedWriter(new FileWriter(outputFile));
			for(int i = 0; i < R.columns; i++) {
				for(int j = database.matrixRows() - 1 ;j >= (database.matrixRows() - maxResults) && j >= 0; j--) {	
					writer.write((combined ? combinedQueriesNames.get(i) : queryNames.get(i)) + " 0 " + databaseNames.get(results[i][j])
							+ " " + (database.matrixRows() - j) + " " + R.get(results[i][j] , i) + " MF\n");
				}
			}
			writer.close();
		}catch(IOException ex) {
			ex.printStackTrace();
		}
	}
	
	/**
	 * 
	 * @param dbIndex
	 * @param d
	 * @param is
	 * @param operator
	 * @return
	 */
	private static double combineScores(int dbIndex, DoubleMatrix d, int[] queryIdexes, Operator operator) {
		double r = 0;
		
		for (int i = 0; i < queryIdexes.length; i++) {
			double score =0;
			try{
				score = d.get(dbIndex, queryIdexes[i]);
			}catch(Exception e){
				e.printStackTrace();
				System.out.println(dbIndex + " " + queryIdexes[i] + " " + queryIdexes.length);
			}
			switch (operator) {
				case avg:
					r = (score + i * r) / (i + 1);
					break;
				case max:
					r = (r > score) ? r : score;
					break;
				case rsum:
					r = score + r;
					break;
				default:
					r = (score + i * r) / (i + 1);
			}
		}
			
		return r;
	}

	public static ArrayList<String> loadNames(String file){
		try{
			BufferedReader reader = new BufferedReader(new FileReader(file));
			ArrayList<String> names = new ArrayList<String>();
			String name;
			while((name = reader.readLine()) != null){
				names.add(name);
			}
			return names;
		}catch(IOException ioe){
			ioe.printStackTrace();
		}
		return null;
	}
	
	/**
	 * 
	 * @param file
	 * @return
	 */
	public static ArrayList<String[]> loadCombinedNames(String file){
		try{
			BufferedReader reader = new BufferedReader(new FileReader(file));
			ArrayList<String[]> names = new ArrayList<String[]>();
			String data;
			while((data = reader.readLine()) != null){
				names.add(data.split(" "));
			}
			return names;
		}catch(IOException ioe){
			ioe.printStackTrace();
		}
		return null;
	}
	
}
